
package brian.huang.reader;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Picture;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Xml;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebView;
import android.webkit.WebView.PictureListener;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;

import brian.huang.reader.ReaderFragment.ContentReader;
import brian.huang.reader.ReaderFragment.ContentReader.Page;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class EpubReader implements ReaderFragment.ContentReader {
    private static final String TAG = "EpubParser";
    private static final String CONTAINER = "META-INF/container.xml";
    private Context mContext;
    private String mFilePath;
    private ZipFile mZipFile;
    private Container mContainer;
    private Manifest mManifest;
    private Metadata mMetadata;
    private Spine mSpine;
    private String[] mChapterList;

    private int mFontSize;
    private int mPageWidth;

    private class Container {
        public String rootfile;
        public String hrefPrefix = "";

        public Container(InputStream container) {
            parse(container);
        }

        private boolean parse(InputStream in) {
            try {
                XmlPullParser parser = Xml.newPullParser();
                parser.setInput(in, null);
                while (parser.next() != XmlPullParser.END_DOCUMENT) {
                    int eventType = parser.getEventType();
                    if (eventType != XmlPullParser.START_TAG) {
                        continue;
                    }
                    String tag = parser.getName();
                    if ("rootfile".equals(tag)) {
                        String fullPath = parser.getAttributeValue(null, "full-path");
                        if (fullPath != null) {
                            rootfile = fullPath;
                            break;
                        }
                    }
                }
            } catch (XmlPullParserException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (rootfile == null) {
                return false;
            }
            int end = rootfile.lastIndexOf("/");
            if (end >= 0) {
                hrefPrefix = rootfile.substring(0, end + 1);
            }
            return true;
        }
    }

    private class Metadata {
        public String title;
        public String language;
        public String identifier;

        public Metadata(InputStream in) {
            parse(in);
        }

        public boolean parse(InputStream in) {
            try {
                XmlPullParser parser = Xml.newPullParser();
                parser.setInput(in, null);
                while (parser.next() != XmlPullParser.END_DOCUMENT) {
                    int eventType = parser.getEventType();
                    if (eventType != XmlPullParser.START_TAG) {
                        continue;
                    }
                    String tag = parser.getName();
                    if ("metadata".equals(tag)) {
                        parseMetadata(parser);
                        break;
                    }
                }
            } catch (XmlPullParserException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (title != null) {
                return true;
            }
            return false;
        }

        private void parseMetadata(XmlPullParser parser) throws XmlPullParserException, IOException {
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.getEventType() != XmlPullParser.START_TAG) {
                    continue;
                }
                String tag = parser.getName();
                if ("title".equals(tag)) {
                    title = readText(parser);
                } else if ("language".equals(tag)) {
                    language = readText(parser);
                } else if ("identifier".equals(tag)) {
                    identifier = readText(parser);
                } else {
                    skip(parser);
                }
            }
        }

        private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
            int depth = 1;
            while (depth != 0) {
                switch (parser.next()) {
                    case XmlPullParser.END_TAG:
                        depth--;
                        break;
                    case XmlPullParser.START_TAG:
                        depth++;
                        break;
                }
            }
        }

        private String readText(XmlPullParser parser) throws XmlPullParserException, IOException {
            String result = null;
            if (parser.next() == XmlPullParser.TEXT) {
                result = parser.getText();
                parser.nextTag();
            }
            return result;
        }
    }

    private class Manifest {
        private class Item {
            private String id;
            private String href;
            private String mediaType;

            public Item(String i, String h, String m) {
                id = i;
                href = h;
                mediaType = m;
            }
        }

        HashMap<String, Item> item;

        public Manifest(InputStream is) {
            item = new HashMap<String, Item>();
            parse(is);
        }

        private boolean parse(InputStream is) {
            try {
                XmlPullParser parser = Xml.newPullParser();
                parser.setInput(is, null);
                while (parser.next() != XmlPullParser.END_DOCUMENT) {
                    int eventType = parser.getEventType();
                    if (eventType != XmlPullParser.START_TAG) {
                        continue;
                    }
                    String tag = parser.getName();
                    if ("manifest".equals(tag)) {
                        parseManifest(parser);
                        break;
                    }
                }
            } catch (XmlPullParserException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (item.size() != 0) {
                return true;
            }
            return false;
        }

        private void parseManifest(XmlPullParser parser) throws XmlPullParserException, IOException {
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.getEventType() != XmlPullParser.START_TAG) {
                    continue;
                }
                String tag = parser.getName();
                if ("item".equals(tag)) {
                    String id = parser.getAttributeValue(null, "id");
                    String href = parser.getAttributeValue(null, "href");
                    String mediaType = parser.getAttributeValue(null, "media-type");
                    Item i = new Item(id, href, mediaType);
                    item.put(id, i);
                    parser.nextTag();
                    // Log.i(TAG, id + ", " + href + ", " + mediaType);
                }
            }
        }
    }

    private class Spine {
        private class Itemref {
            private String idref;

            public Itemref(String id) {
                idref = id;
            }
        }

        List<Itemref> itemref;

        public Spine(InputStream is) {
            itemref = new ArrayList<Itemref>();
            parse(is);
        }

        private boolean parse(InputStream in) {
            try {
                XmlPullParser parser = Xml.newPullParser();
                parser.setInput(in, null);
                while (parser.next() != XmlPullParser.END_DOCUMENT) {
                    int eventType = parser.getEventType();
                    if (eventType != XmlPullParser.START_TAG) {
                        continue;
                    }
                    String tag = parser.getName();
                    if ("spine".equals(tag)) {
                        parseSpine(parser);
                        break;
                    }
                }
            } catch (XmlPullParserException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (itemref.size() != 0) {
                return true;
            }
            return false;
        }

        private void parseSpine(XmlPullParser parser) throws XmlPullParserException, IOException {
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.getEventType() != XmlPullParser.START_TAG) {
                    continue;
                }
                String tag = parser.getName();
                if ("itemref".equals(tag)) {
                    String idref = parser.getAttributeValue(null, "idref");
                    Itemref item = new Itemref(idref);
                    itemref.add(item);
                    parser.nextTag();
                }
            }
        }
    }

    public class WebViewPage implements ContentReader.Page {
        protected static final int MSG_TEST = 0;
        protected static final int MSG_WAIT_LOAD_COMPLETE = 1;
        protected static final int MSG_LOAD_COMPLETE = 2;
        private EpubReader mReader;
        private View mView;
        private LoadCompletedListener mListener;
        private WebViewPage mPage;

        private int mSpineRef;
        private int mTotalPage;
        private int mCurrentPage;

        private boolean mLoadCompleted;

        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                WebView view = (WebView)mView.findViewById(R.id.web_view);
                String js;
                switch (msg.what) {
                    case MSG_TEST:
                    	break;
                    case MSG_WAIT_LOAD_COMPLETE:
                        Log.i(TAG, "WAIT_LOAD_COMPLETE ");
                        view = (WebView)mView.findViewById(R.id.web_view);

                        js = "javascript:function formatPages() {" +
                                "var d = document.getElementsByTagName('body')[0];" +
                                "var desiredHeight = window.innerHeight;"+
                                "var desiredWidth = window.innerWidth;" +
                                "window.HTMLOUT.debug(desiredWidth);" +
                                "d.style.fontSize = " + mReader.getFontSize() + "+'%';" +
                                "d.style.height = desiredHeight+'px';" +
                                "d.style.margin = 0;" +
                                "d.style.paddingTop = 1+'px';" +
                                "d.style.paddingBottom = 1+'px';" +
                                "d.style.paddingLeft = 0+'px';" +
                                "d.style.paddingRight = 0+'px';" +
                                "d.style.WebkitColumnWidth = desiredWidth+'px';" +
                                "d.style.WebkitColumnGap = 0;" +
                                "}";
                        view.loadUrl(js);
                        view.loadUrl("javascript:formatPages()");

                        String scrollX = "desiredWidth * " + (mCurrentPage - 1);
                        if (mCurrentPage == 0) {
                            scrollX = "document.getElementsByTagName('body')[0].scrollWidth - desiredWidth";
                        }
                    	js = "javascript:function scrollPage() {" +
								"var d = document.getElementsByTagName('body')[0];" +
								"var desiredWidth = window.innerWidth;" +
								"var scrollX = " + scrollX + ";" +
								"var scrollY = 0;" +
								"window.scrollTo(scrollX, scrollY);" +
								"}";
						view.loadUrl(js);
						view.loadUrl("javascript:scrollPage()");

						js = "javascript:function callbackToJava() {" +
						        "var contentWidth = document.getElementsByTagName('body')[0].scrollWidth;" +
                                "var desiredWidth = window.innerWidth;" +
                                "var totalPage = contentWidth / desiredWidth;" +
                                "window.HTMLOUT.setPageCount(totalPage);" +
                                "}";
                        view.loadUrl(js);
                        view.loadUrl("javascript:callbackToJava()");
                        break;
                    case MSG_LOAD_COMPLETE:
                    	Log.i(TAG, "LOAD_COMPLETE");
                    	ProgressBar progressBar = (ProgressBar)mView.findViewById(R.id.progress_bar);
                        progressBar.setVisibility(View.GONE);
                        if (mListener != null) {
                            mListener.onLoadCompleted(mView, mPage);
                        }
                        mLoadCompleted = true;
                        break;
                }
            }
        };

        public WebViewPage(ContentReader reader) {
            LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View pageView = inflater.inflate(R.layout.page_view, null);
            mReader = (EpubReader) reader;
            mView = pageView;

            // Config webview.
            WebView webView = (WebView)mView.findViewById(R.id.web_view);
            int pageWidth = reader.getPageWidth();
            if (pageWidth > 0) {
                LayoutParams params = webView.getLayoutParams();
                params.width = pageWidth;
                webView.setLayoutParams(params);
            }
            webView.getSettings().setJavaScriptEnabled(true);
            webView.getSettings().setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS);
            webView.setHorizontalScrollBarEnabled(false);
            webView.setVerticalScrollBarEnabled(false);
            webView.addJavascriptInterface(new JavaScriptInterface(), "HTMLOUT");
            webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            webView.setOnTouchListener(new OnTouchListener() {
				public boolean onTouch(View v, MotionEvent event) {
					return true;
				}
            });

            mListener = null;
            mPage = this;

            mSpineRef = 0;
            mTotalPage = -1;
            mCurrentPage = 1;

            mLoadCompleted = false;
        }

        public WebViewPage(ContentReader reader, String bookmark) {
            this(reader);

            if (bookmark != null) {
                File file = new File(mReader.mFilePath);
                SharedPreferences setting = mContext.getSharedPreferences(
                        file.getName() + "_" + file.lastModified(), 0);
                mSpineRef = setting.getInt("bookmark_spine_ref_" + bookmark, 0);
                mTotalPage = setting.getInt("bookmark_total_page_" + bookmark, -1);
                mCurrentPage = setting.getInt("bookmark_current_page_" + bookmark, 1);
            }
        }

        public WebViewPage(ContentReader reader, int chapter) {
            this(reader);

            mSpineRef = chapter;
        }

        public WebViewPage(WebViewPage from) {
            this(from.mReader);

            mSpineRef = from.mSpineRef;
            mTotalPage = from.mTotalPage;
            mCurrentPage = from.mCurrentPage;
        }

        private boolean next() {
            mCurrentPage++;
            if (mCurrentPage > mTotalPage) {
            	mTotalPage = -1;
                mCurrentPage = 1;
                mSpineRef++;
            }

            return true;
        }

        private boolean previous() {
            mCurrentPage--;
            if (mCurrentPage < 1) {
            	mTotalPage = -1;
                mCurrentPage = 0;
                mSpineRef--;
            }

            return true;
        }

        public boolean isFirst() {
            return mSpineRef == 0 && mCurrentPage == 1;
        }

        public boolean isLast() {
            int spineSize = mReader.getSpine().itemref.size();
            return mSpineRef == (spineSize - 1) && mCurrentPage >= mTotalPage;
        }

        public boolean saveBookmark(String id) {
            File file = new File(mReader.mFilePath);
            SharedPreferences settings = mContext.getSharedPreferences(
                    file.getName() + "_" + file.lastModified(), 0);
            SharedPreferences.Editor editor = settings.edit();
            editor.putInt("bookmark_spine_ref_" + id, mSpineRef);
            editor.putInt("bookmark_total_page_" + id, mTotalPage);
            editor.putInt("bookmark_current_page_" + id, mCurrentPage);
            editor.commit();
            return false;
        }

        private void loadContent(int ref) {
            Spine spine = mReader.getSpine();
            ref = Math.min(Math.max(0, ref), spine.itemref.size() - 1);
            mSpineRef = ref;

            new AsyncTask<Integer, Void, String>() {
                @Override
                protected String doInBackground(Integer... params) {
                    String path = mReader.getContentBySpineRef(params[0]);
                    return path;
                }

                @Override
                protected void onPostExecute(String result) {
                    WebView view = (WebView)mView.findViewById(R.id.web_view);
                    view.loadUrl("file://" + result);
                    view.setWebViewClient(new WebViewClient() {
						@Override
						public void onPageFinished(WebView view, String url) {
							mHandler.sendEmptyMessage(MSG_WAIT_LOAD_COMPLETE);
						}
                    });
                }
            }.execute(ref);
        }

        class JavaScriptInterface {
        	public void setPageCount(int totalPage) {
        		Log.i(TAG, "setPageCount:" + totalPage);
        		if (mTotalPage == -1) {
        			mTotalPage = totalPage;
        			if (mCurrentPage == 0) {
        				mCurrentPage = totalPage;
        			}
        		} else {
        			mCurrentPage = totalPage * mCurrentPage / mTotalPage;
        			mTotalPage = totalPage;
        		}
        		mHandler.sendEmptyMessage(MSG_LOAD_COMPLETE);
        	}

        	public void debug(String msg) {
        	    Log.i(TAG, "JavaScriptInterface debug: " + msg);
        	}
        }

        public int getChapterIndex() {
            return mSpineRef;
        }

        public View getView() {
            return mView;
        }

        public Page getNextPage() {
            if (isLast()) {
                return null;
            }
            WebViewPage result = new WebViewPage(this);
            result.next();
            return result;
        }

        public Page getPreviousPage() {
            if (isFirst()) {
                return null;
            }
            WebViewPage result = new WebViewPage(this);
            result.previous();
            return result;
        }

        public void setLoadCompletedListener(LoadCompletedListener listener) {
            mListener = listener;
        }

        @Override
        public void loadPage() {
            if (!mLoadCompleted) {
                loadContent(mSpineRef);
            }
        }

        @Override
        public int getPageNumber() {
            return mCurrentPage;
        }

        @Override
        public int getTotalPage() {
            return mTotalPage;
        }
    }

    public EpubReader(Context context, String filePath) {
        mFilePath = filePath;
        mContext = context;
        try {
            mZipFile = new ZipFile(filePath);
            ZipEntry ze = mZipFile.getEntry(CONTAINER);
            InputStream is = mZipFile.getInputStream(ze);
            mContainer = new Container(is);
            is.close();
            if (mContainer.rootfile == null) {
                return;
            }
            ze = mZipFile.getEntry(mContainer.rootfile);
            is = mZipFile.getInputStream(ze);
            mMetadata = new Metadata(is);
            is.close();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }

    public String getTitle() {
        String title = mMetadata.title;
        return title;
    }

    private String getContentBySpineRef(int ref) {
        Spine spine = getSpine();
        Spine.Itemref itemref = (Spine.Itemref) spine.itemref.get(ref);
        Manifest.Item item = mManifest.item.get(itemref.idref);
        String entryName = mContainer.hrefPrefix + item.href;
        ZipFileUtils unzip = new ZipFileUtils(mContext.getFilesDir().getAbsolutePath(), mFilePath);
        String path = unzip.getEntry(entryName);
        return path;
    }

    private Spine getSpine() {
        Spine result = null;
        try {
            if (mSpine == null || mManifest == null) {
                ZipEntry ze = mZipFile.getEntry(mContainer.rootfile);
                InputStream is = mZipFile.getInputStream(ze);
                mManifest = new Manifest(is);
                is.close();

                ze = mZipFile.getEntry(mContainer.rootfile);
                is = mZipFile.getInputStream(ze);
                mSpine = new Spine(is);
                is.close();
            }
            result = mSpine;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    public String[] getChapterList() {
        if (mChapterList != null) {
            return mChapterList;
        }
        int spineSize = getSpine().itemref.size();
        String[] result = new String[spineSize];
        for (int i = 0; i < spineSize; i++) {
            result[i] = getSpine().itemref.get(i).idref;
        }
        mChapterList = result;
        return mChapterList;
    }

    public Page getPage(String bookmark) {
        return new WebViewPage(this, bookmark);
    }

    public Page getPage(int chapter) {
        return new WebViewPage(this, chapter);
    }

    @Override
    public void setPageWidth(int width) {
        mPageWidth = width;
    }

    @Override
    public int getPageWidth() {
        return mPageWidth;
    }

    @Override
    public void setFontSize(int fontSize) {
        mFontSize = fontSize;
    }

    @Override
    public int getFontSize() {
        return mFontSize;
    }
}
